[Redis]Redis基础

NoSQL

  • NoSQL全称是Not Only SQL(不仅仅是SQL)它是一种非关系型数据库,相比传统SQL关系型数据库,它:

    • 不保证关系数据的ACID特性。
    • 并不遵循SQL标准
    • 消除数据之间关联性
  • 优势:

    • 远超传统关系数据库的性能。
    • 非常易于扩展。
    • 数据模型更加灵活。
    • 高可用。
  • Redis是一个开源的键值存储数据库,所有的数据全部存放在内存中,它的性能大大磁盘IO,并且他也可以支持数据持久化。

    • 键值存储数据库: 所有的数据都是以键值方式存储的,类似于我们之前学过的HashMap,使用起来非常简单方便,性能也非常高。

数据类型

  • 一个键值对除了存储一个String类型的值以外,还支持多种常用的数据类型。

Hash

  • 这种类型本质上就是一个HashMap,也就是嵌套了一个HashMap罢了。
    1
    2
    3
    4
    #Redis默认存String类似于这样:
    Map<String, String> hash = new HashMap<>();
    #Redis存Hash类型的数据类似于这样:
    Map<String, Map<String, String>> hash = new HashMap<>();
  • 我们可以像这样来添加一个Hash类型的数据:

    1
    hset <key> [<字段> <>]...
  • 获取:

    1
    2
    3
    hget <key> <字段>
    -- 如果想要一次性获取所有的字段和值
    hgetall <key>
  • 判断存在:
    1
    hexists <key> <字段>
  • 删除:
    1
    hdel <key>
  • 获取存储多少个键值对:
    1
    hlen <key>
  • 我们也可以一次性获取所有字段的值:

    1
    hvals <key>
  • 要注意的是,Hash中只能存放字符串值,不允许出现嵌套的的情况。

List

  • 我们可以直接向一个已存在或是不存在的List中添加数据,如果不存在,会自动创建:
    1
    2
    3
    4
    5
    6
    -- 向列表头部添加元素
    lpush <key> <element>...
    -- 向列表尾部添加元素
    rpush <key> <element>...
    -- 在指定元素前面/后面插入元素
    linsert <key> before/after <指定元素> <element>
  • 同样的,获取元素也非常简单:
    1
    2
    3
    4
    5
    6
    7
    8
    -- 根据下标获取元素
    lindex <key> <下标>
    -- 获取并移除头部元素
    lpop <key>
    -- 获取并移除尾部元素
    rpop <key>
    -- 获取指定范围内的
    lrange <key> start stop
  • 注意下标可以使用负数来表示从后到前数的数字:
    1
    2
    -- 获取列表a中的全部元素
    lrange a 0 -1

Set和SortedSet

  • Set集合其实就像Java中的HashSet一样(我们在JavaSE中已经讲解过了,HashSet本质上就是利用了一个HashMap,但是Value都是固定对象,仅仅是Key不同)
  • 它不允许出现重复元素,不支持随机访问,但是能够利用Hash表提供极高的查找效率。
  • 那么如果我们要求Set集合中的数据按照我们指定的顺序进行排列怎么办呢?这时就可以使用SortedSet,它支持我们为每个值设定一个分数,分数的大小决定了值的位置,所以它是有序的。

持久化

  • Redis数据库中的数据都是存放在内存中,但容易丢失。
  • 就需要持久化,我们需要将我们的数据备份到硬盘上,防止断电或是机器故障导致的数据丢失。
  • 持久化的方式有两种:
    • 直接保存当前以及存储的数据 ,相当于复制内存中的数据到硬盘上,需要恢复数据时直接读取即可。
    • 保存我们存放数据的所有过程 ,需要恢复数据时,只需要将整个过程完整地重演一遍就能保证与之前数据库中的内容一致。

RDB

  • RDB就是第一种解决方案。
  • 如何将数据保存到本地呢?我们可以使用命令:
    1
    2
    3
    save
    -- 注意上面这个命令是直接保存,会占用一定的时间,也可以单独开一个子进程后台执行保存
    bgsave
  • 执行后,会在服务端目录下生成一个dump.rdb文件,而这个文件中就保存了内存中存放的数据。
  • 当服务器重启后,会自动加载里面的内容到对应数据库中。保存后我们可以关闭服务器:

    1
    shutdown

  • 虽然这种方式非常方便,但是由于会完整复制所有的数据,如果数据库中的数据量比较大,那么复制一次可能就需要花费大量的时间,所以我们可以每隔一段时间自动进行保存;

  • 如果我们基本上都是在进行读操作,而没有进行写操作,实际上只需要偶尔保存一次即可,因为数据几乎没有怎么变化,可能两次保存的都是一样的数据。

AOF

  • 虽然RDB能够很好地解决数据持久化的问题,但是它的缺点也很明显:
    • 每次都需要去完整地保存整个数据库的数据,同时后保存过程中也会产生额外的内存开销。
    • 最严重的是他不是事实保存的,如果在自动保存触发之前崩溃,依然会导致少量数据丢失。
  • AOF就是另外一种方式,它会以日志的形式将我们每次执行的命令都进行保存,服务器重启时会将所有命令一次执行,通过这种重演的方法将数据恢复,这样就能很好解决实时性存储问题。

  • 多久写一次日志呢,有三种策略:

    • always:每次执行写操作都会保存一次。
    • everysec:美妙保存一次(默认配置)
    • no:看系统心情保存。
  • 可以在配置文件中配置:

    1
    2
    3
    4
    5
    6
    # 注意得改成也是
    appendonly yes

    # appendfsync always
    appendfsync everysec
    # appendfsync no

    重启服务器后,可以看到服务器目录下多了一个appendonly.aof文件,存储的就是我们执行的命令。

  • 缺点:
    • 每次服务器启动都需要进行过程重演,相比RDB更加耗费时间。
    • 并且随着我们的操作变多,不断累计,可能到最后我们的aof文件会变得无比巨大,我们需要一个改进方案来优化这些问题。
  • Redis有一个AOF重写机制进行优化,比如我们执行了这样的语句:
    1
    2
    3
    lpush test 666
    lpush test 777
    lpush test 888
  • 实际上一条语句就能实现:
    1
    lpush test 666 777 888

总结

  • AOF:
    • 优点:存储速度快,消耗资源少,支持实时存储。
    • 缺点:加载速度慢、数据体积大。
  • RDB:
    • 优点:加载速度快、数据体积小。
    • 缺点:存储速度慢且大量消耗资、会发生数据丢失。

事务和锁机制

  • 开启事务:
    1
    multi
  • 执行事务:
    1
    exec
  • 中途取消事务
    1
    exec
  • 实际上整个事务是创建了一个命令队列
  • 不像MySQL那种在事务中也能单独得到结果,而是我们提前将所有的命令装在队列中,但是并不会执行,而是等我们提交事务的时候再统一执行。

  • Redis中的锁是一种乐观锁。

    • 乐观锁:并不认为会有人来抢占资源,所以会直接对数据进行操作,在操作时再去验证是否有其他人抢占资源。
    • 悲观锁:时刻认为别人会来抢占资源,禁止一切外来访问,直到释放锁,具有强烈的排他性质。
  • Redis中可以使用watch来监视一个目标,如果执行事务之前被监视目标发生了修改,则取消本次事务:

    1
    watch
  • 取消监视可以使用
    1
    unwatch

使用Redis做二级缓存

  • 我们可以将Redis作为Mybatis的二级缓存,这样就能实现多台服务器使用同一个二级缓存,因为它们只需要连接同一个Redis服务器即可。
  • 所有的缓存数据全部存储在Redis服务器上。我们需要手动实现Mybatis提供的Cache接口,这里我们简单编写一下:

三大缓存问题

缓存穿透

  • 当我们查询一个一定不存在的数据,由于缓存不命中,接着查询数据库也无法查询出结果,因此也不会写入到缓存中,这将导致每个查询都会去请求数据库,造成缓存穿透。
  • 用户想要查询一个数据,发现redis内存数据库没有,也就是缓存没有命中,于是向持久层数据库查询。发现也没有,于是本次查询失败。
  • 当用户很多的时候,缓存都没有命中,于是都去请求了持久层数据库。这会给持久层数据库造成很大的压力,这时候就相当于出现了缓存穿透。

  • 解决方案:

    • 缓存空对象:
      • 为这个key设置一个空值,同时写入redis,下次请求的时候就不会访问数据库。
      • 但是如果每次请求的是不同的key,同时这个key在数据库中也是不存在的,那这样依然会发生缓存穿透。
    • 布隆过滤:
      • 用一个很长的二进制向量和一系列随机映射函数实现。
      • 将元素插入集合时,对元素进行多次哈希运算,将得到的哈希值在对应的向量上设置为1。
      • 判断元素是否存在于集合时,同样进行多次哈希运算,得到多个哈希值,并在向量上检查是否都为1.
      • 如果都为1,则该元素可能存在集合中,反智一定不存在于集合中。
      • 有一定的误判率,可能吧不存在于集合的元素误判为存在集合中,但不会误判存在集合中的元素。

缓存击穿

  • 某个key属于热点数据,同一时间很多人访问。
  • 在这个Key失效的瞬间,大量的请求到来,这时发现缓存中没有数据,就全部直接请求到数据库。
  • 相当于击穿了缓存屏障,直接攻击整个系统核心。
  • 解决方法:
    • 最好的解决办法就是不让Key那么快过期,如果一个Key处于高频访问,那么可以适当地延长过期时间。

缓存雪崩

  • 当Redis服务器炸了或者大量的key在同一时间失效,那么如果这时又有很多的请求来访问不同的数据,同一时间内缓存服务器就得向数据库大量发起请求来重新建立缓存,容易造成服务器崩溃。
  • 解决方案:
    • 设置自动更新:当缓存数据过期时,自动向数据库查询最小数据并更新到缓存中,避免在缓存失效时,大量请求直接达到数据库上。
    • 设置缓存失效时间随机性。
    • 使用预存预热机制:提前将一些常用的缓存数据加载到缓存中。
    • 设置多集缓存:将热点数据放在高速缓存中,冷门数据放在低速缓存中。
    • 使用限流机制:限制系统的请求并发性,避免请求过多导致系统宕机。